package edu.uw.cs.cse461.sp12.OS;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import edu.uw.cs.cse461.sp12.DB461.DB461.DB461Exception;
import edu.uw.cs.cse461.sp12.SNetDB.SNetDB461;
import edu.uw.cs.cse461.sp12.SNetDB.SNetDB461.CommunityRecord;
import edu.uw.cs.cse461.sp12.SNetDB.SNetDB461.PhotoRecord;
import edu.uw.cs.cse461.sp12.SNetDB.SNetDB461.Photo;
import edu.uw.cs.cse461.sp12.util.Base64;
import edu.uw.cs.cse461.sp12.util.Log;

public class SNetControl {
	private static final String TAG = "SNetController";
	
	/** Used to keep track of DDNS names */
	private static DDNSResolverService resolver = null;
	/** Used to reference self in DB */
	private static DDNSFullName self = null;
	
	public static void setup() throws Exception {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			
			// Check root and self exist
			DDNSFullName root = new DDNSFullName("");
			db.registerMember(root.getName());
			self = new DDNSFullName(OS.hostname());
			db.registerMember(self.getName());
			
			// Get the resolver service
			resolver = (DDNSResolverService) OS.getService("ddnsresolver");
			
			// Fetch initial updates from root
			contact(root);
		} catch (Exception e) {
			Log.d(TAG, "setup caught Exception: " + e.getMessage());
			e.printStackTrace();
			throw e;
		} finally {
			if (db != null) db.discard();
		}
	}

	public static void befriend(DDNSFullName name) throws DB461Exception {
		changeFriendStatus(name, true);
	}
	
	public static void contact(DDNSFullName host) throws Exception {
		JSONArray photoUpdates = fetchUpdates(host);
		Log.d(TAG, "Photos to fetch: " + photoUpdates.toString());
		for (int i = 0; i < photoUpdates.length(); i++) {
			// fetchPhoto
			int photoHash = photoUpdates.getInt(i);
			Log.d(TAG, "fetching photo with hash: " + photoHash);
			fetchPhoto(host, photoHash);
		}
		
	}
	
	public static String dumpDB() {
		StringBuilder result = new StringBuilder();
		
		result.append("SNET DB");
		result.append("\n\n");
		
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			
			// uri-specific handling
			
			result.append("\nGallery:\n");
			for(PhotoRecord photo : SNetControl.getGallery().values()) {
				result.append("\t");
				result.append(photo.toString());
				result.append("\n");
			}
			result.append("\nCommunity:\n");
			for(CommunityRecord record : db.COMMUNITYTABLE.readAll()) {
				result.append("\t");
				result.append(record.toString());
				result.append("\n");
			}
			result.append("\nPhotos:\n");
			for(PhotoRecord record : db.PHOTOTABLE.readAll()) {
				result.append("\t");
				result.append(record.toString());
				result.append("\n");
			}
		} catch (Exception e) {
			result.append("Error: " + e.getMessage());
			e.printStackTrace();
		} finally {
			if (db != null) db.discard();
		}
		
		return result.toString();
	}
	
	public static void fetchPhoto(DDNSFullName host, int hash) throws Exception {
		// Get the location of the host (will throw exception or resurn result)
		DDNSRRecord hostRecord = resolver.resolve(host);
		
		JSONObject request = new JSONObject();
		request.put("photohash", hash);
		
		Log.d(TAG, "fetchPhoto Request: " + request.toString());
		
		RPCCallerSocket socket = new RPCCallerSocket(hostRecord.getFullName().getName(), 
													hostRecord.getIP(), 
													Integer.toString(hostRecord.getPort()));
		JSONObject response = socket.invoke("snet", "fetchPhoto", request);
		socket.close();
		
		Log.d(TAG, "fetchPhoto Response: " + response.toString());
		
		int responseHash = response.getInt("photohash");
		if (responseHash != hash) {
			throw new RuntimeException("Response Hash (" + responseHash + ") is not equal to Request Hash (" + hash + ")");
		}
		// Store the photo in a file
		String photoData = response.getString("photodata");
		byte[] rawPhotoData = Base64.decode(photoData);
		File photoFile = generatePhotoFile(responseHash);
		FileOutputStream output = new FileOutputStream(photoFile);
		output.write(rawPhotoData);
		output.close();
		
		Log.d(TAG, "Stored photo: " + photoFile.getAbsolutePath());
		
		// Store in database
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			
			PhotoRecord record = db.PHOTOTABLE.readOne(hash);
			record.file = photoFile;
			commit(db, record);
		} finally {
			if (db != null) db.discard();
		}
	}
	
	public static JSONArray fetchUpdates(DDNSFullName host) throws Exception {
		// Get the location of the host (will throw exception or return result)
		DDNSRRecord hostRecord = resolver.resolve(host);
		
		JSONObject request = new JSONObject();
		// Pass in null since don't have any members to compare to
		request.put("community", getMembersJSON(null));
		request.put("needphotos", getNeededPhotosArray());
		
		Log.d(TAG, "fetchUpdates Request: " + request.toString());
		
		RPCCallerSocket socket = new RPCCallerSocket(hostRecord.getFullName().getName(), 
													hostRecord.getIP(), 
													Integer.toString(hostRecord.getPort()));
		JSONObject response = socket.invoke("snet", "fetchUpdates", request);
		socket.close();
		
		Log.d(TAG, "fetchUpdates Response: " + response.toString());
		
		// Update all of the community members
		updateMembers(response.getJSONObject("communityupdates"));
		
		JSONArray returnedPhotoUpdates = response.getJSONArray("photoupdates");
		// Sort through returned updates and only return array of photos that we actually want
		Set<Integer> neededPhotos = getNeededPhotos();
		Set<Integer> resultPhotoUpdates = new HashSet<Integer>();
		for (int i = 0; i < returnedPhotoUpdates.length(); i++) {
			int hash = returnedPhotoUpdates.getInt(i);
			
			if (neededPhotos.contains(hash)) {
				// They do have one of our new needed photos
				resultPhotoUpdates.add(hash);
			}
		}
		return toJSONArray(resultPhotoUpdates);
	}
	
	public static JSONArray toJSONArray(Set<Integer> photos) {
		// Go through photos and put in JSONArray
		JSONArray result = new JSONArray();
		for (int hash : photos) {
			result.put(hash);
		}
		
		return result;
	}
	
	public static void fixDB() throws DB461Exception {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			
			db.checkAndFixDB(OS.galleryDir());
		} finally {
			if (db != null) db.discard();
		}
	}
	
	public static List<DDNSFullName> getFriendsList() throws DB461Exception {
		List<DDNSFullName> result = new ArrayList<DDNSFullName>();
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			
			for (CommunityRecord friend : db.COMMUNITYTABLE.readFriends(self.getName())) {
				result.add(new DDNSFullName(friend.name));
			}
			
			return result;
		} finally {
			if (db != null) db.discard();
		}
	}

	public static Map<Integer, PhotoRecord> getGallery() throws DB461Exception {
		// Use a TreeSet so result will be sorted
		Map<Integer, PhotoRecord> result = new HashMap<Integer, PhotoRecord>();
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			
			for (CommunityRecord record : db.COMMUNITYTABLE.readAll()) {
				if (record.isFriend || record.name.equals(self.getName())) {
					// Only list friend's and own pictures
					
					if (record.myPhotoHash != 0) {
						// Add their personal picture
						PhotoRecord friendPhoto = db.PHOTOTABLE.readOne(record.myPhotoHash);
						if (friendPhoto == null) {
							throw new DB461Exception("personal PhotoRecord for friend " + record.name + 
														" could not be found");
						}
						result.put(friendPhoto.hash, friendPhoto);
					}
					
					if (record.chosenPhotoHash != 0) {
						// And their chosen picture
						PhotoRecord friendChosenPhoto = db.PHOTOTABLE.readOne(record.chosenPhotoHash);
						if (friendChosenPhoto == null) {
							throw new DB461Exception("chosen PhotoRecord for friend " + record.name + 
														" could not be found");
						}
						result.put(friendChosenPhoto.hash, friendChosenPhoto);
					}
				}
			}
		} finally {
			if (db != null) db.discard();
		}
		return result;
	}
	
	public static List<DDNSFullName> getMembersList() throws DB461Exception {
		List<DDNSFullName> result = new ArrayList<DDNSFullName>();
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			
			for (CommunityRecord friend : db.COMMUNITYTABLE.readAll()) {
				result.add(new DDNSFullName(friend.name));
			}
			
			return result;
		} finally {
			if (db != null) db.discard();
		}
	}

	public static File getMyPhoto() throws DB461Exception {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
		
			// Get reference to our own record
			CommunityRecord selfRecord = db.COMMUNITYTABLE.readOne(self.getName());
			File myPhotoFile = getPhoto(selfRecord.myPhotoHash);
			return myPhotoFile;
		} finally {
			if (db != null) db.discard();
		}
	}
	
	public static int getGeneration() throws DB461Exception {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
		
			// Get reference to our own record
			CommunityRecord selfRecord = db.COMMUNITYTABLE.readOne(self.getName());
			return selfRecord.generation;
		} finally {
			if (db != null) db.discard();
		}
	}
	
	public static File getChosenPhoto() throws DB461Exception {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
		
			// Get reference to our own record
			CommunityRecord selfRecord = db.COMMUNITYTABLE.readOne(self.getName());
			File chosenPhotoFile = getPhoto(selfRecord.chosenPhotoHash);
			return chosenPhotoFile;
		} finally {
			if (db != null) db.discard();
		}
	}
	
	public static File getPhoto(int photoHash) throws DB461Exception {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
		
			PhotoRecord photo = db.PHOTOTABLE.readOne(photoHash);
			if (photo != null && photo.file != null && photo.file.exists()) {
				return photo.file;
			} else {
				return null;
			}
		} finally {
			if (db != null) db.discard();
		}
	}
	
	public static void setChosenPhoto(int chosenPhotoHash) 
						throws DB461Exception, FileNotFoundException, IOException {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			
			CommunityRecord selfRecord = db.COMMUNITYTABLE.readOne(self.getName());
			
			// If something fails, throw exception
			changeChosenPhotoReference(db, chosenPhotoHash, selfRecord, false);
			
			commit(db, selfRecord);
		} finally {
			if (db != null) db.discard();
		}
	}

	public static void setMyPhoto(File myPhotoFile) 
							throws DB461Exception, FileNotFoundException, IOException {
		if (!myPhotoFile.isFile()) {
			throw new RuntimeException(myPhotoFile.toString() + " is not a file!");
		}
		
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			
			CommunityRecord selfRecord = db.COMMUNITYTABLE.readOne(self.getName());
			
			changeMyPhotoReference(db, myPhotoFile, null, selfRecord);
			
			commit(db, selfRecord);
		} finally {
			if (db != null) db.discard();
		}
	}
	
	public static void unfriend(DDNSFullName name) throws DB461Exception {
		changeFriendStatus(name, false);
	}

	public static void updateMembers(JSONObject communityUpdates) throws DB461Exception, JSONException {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();

			Map<DDNSFullName, CommunityRecord> membersToCommit = new HashMap<DDNSFullName, CommunityRecord>();
			Iterator iterator = communityUpdates.keys();
			while (iterator.hasNext()) {
				String memberName = (String) iterator.next();
				DDNSFullName memberFullName = new DDNSFullName(memberName);
				
				JSONObject jsonMember = communityUpdates.getJSONObject(memberName);
				int generation = jsonMember.getInt("generation");

				Log.d(TAG, "Checking updating member: " + memberName + " " + jsonMember.toString());
				if (!membersToCommit.containsKey(memberFullName) || 
						membersToCommit.get(memberFullName).generation < generation) {
					// Only update name if not already updating them 
					// (or if we are, but this other name has a more recent generation than previous)
					
					CommunityRecord member = db.COMMUNITYTABLE.readOne(memberFullName.getName());
					if (member == null) {
						// New member
						member = db.COMMUNITYTABLE.createRecord();
						member.name = memberFullName.getName();
						member.isFriend = false;
						// Set generation to -2 now so that we will always update new members even if they haven't 
						// generated anything yet
						member.generation = -2;
					}
					
					if (member.generation < generation) {
						// Have updates to put in
						Log.d(TAG, "Decided to update member: " + memberName + " " + jsonMember.toString());
						if (membersToCommit.containsKey(memberFullName)) {
							Log.d(TAG, "Previous generation: " + membersToCommit.get(memberFullName).generation);
						}
						
						// Create empty records for their photos
						try {
							changeMyPhotoReference(db, null, jsonMember.getInt("myphotohash"), member);
						} catch (FileNotFoundException e) {
							throw new RuntimeException("Should not have FileNotFoundException", e);
						} catch (IOException e) {
							throw new RuntimeException("Should not have IOException", e);
						}
						changeChosenPhotoReference(db, jsonMember.getInt("chosenphotohash"), member, true);
						
						member.generation = generation;

						membersToCommit.put(memberFullName, member);

						Log.d(TAG, "Updating member: " + member.toString());
					}
				}
			}
			
			// Commit everything
			for (CommunityRecord member : membersToCommit.values()) {
				commit(db, member);
			}
		} finally {
			if (db != null) db.discard();
		}
	}

	private static void changeChosenPhotoReference(SNetDB461 db, int newChosenPhotoHash, 
							CommunityRecord member, boolean createNewRecord) throws DB461Exception {
		PhotoRecord previousChosenPhotoRecord = db.PHOTOTABLE.readOne(member.chosenPhotoHash);
		// need to decrement old chosen photo refCount
		decrementRefCount(previousChosenPhotoRecord);
		
		PhotoRecord chosenPhotoRecord = db.PHOTOTABLE.readOne(newChosenPhotoHash);
		if (chosenPhotoRecord == null) {
			// Could not find record for hash
			if (createNewRecord) {
				// Create a new record for the chosenPhoto
				chosenPhotoRecord = db.createPhotoRecord();
				chosenPhotoRecord.hash = newChosenPhotoHash;
				chosenPhotoRecord.refCount = 1;
				chosenPhotoRecord.file = null;
			} else {
				// Throw an exception
				throw new RuntimeException("Could not find record for photo with hash: " + newChosenPhotoHash);
			}
		} else if (previousChosenPhotoRecord != null && previousChosenPhotoRecord.hash == chosenPhotoRecord.hash) {
			// Chose the same photo again.  Handle this by leaving chosenPhoto same and 
			// making previousChosenPhoto null so that we don't commit changes to it
			previousChosenPhotoRecord = null;
		} else {
			// Need to increment new chosen photo refCount
			incrementRefCount(chosenPhotoRecord);
		}

		// Update record for self
		member.chosenPhotoHash = chosenPhotoRecord.hash;
		member.generation++;
		
		// Write everything at the end
		commit(db, previousChosenPhotoRecord);
		commit(db, chosenPhotoRecord);
	}

	private static void changeFriendStatus(DDNSFullName name, boolean newStatus) throws DB461Exception {
		SNetDB461 db = null;
		try {
			db = new SNetDB461();
			
			CommunityRecord friend = db.COMMUNITYTABLE.readOne(name.getName());
			friend.isFriend = newStatus;
			commit(db, friend);
		} finally {
			if (db != null) db.discard();
		}
	}

	private static void changeMyPhotoReference(SNetDB461 db, File myPhotoFile, Integer myPhotoHash, CommunityRecord member) 
												throws DB461Exception, IOException, FileNotFoundException {
		PhotoRecord previousMyPhotoRecord = db.PHOTOTABLE.readOne(member.myPhotoHash);
		// need to decrement old myPhoto refCount
		decrementRefCount(previousMyPhotoRecord);
		
		File file = null;
		Integer hash = null;
		if (myPhotoFile != null) {
			// They gave us a file to add
			Photo myPhoto = new Photo(myPhotoFile);
			file = myPhoto.file();
			hash = myPhoto.hash();
		} else if (myPhotoHash != null) {
			// They gave us a hash to add
			file = null;
			hash = myPhotoHash;
		} else {
			throw new RuntimeException("Must have either myPhotoFile or myPhotoHash be NonNull");
		}
		
		// Create record for photo
		PhotoRecord myPhotoRecord = db.PHOTOTABLE.readOne(hash);
		if (myPhotoRecord == null) {
			// Need to create new record
			myPhotoRecord = db.PHOTOTABLE.createRecord();
			myPhotoRecord.hash = hash;
			myPhotoRecord.refCount = 0;
		}
		myPhotoRecord.file = file;
		// Always initialize refCount to 1 since self references it
		myPhotoRecord.refCount++;
		Log.d(TAG, "New myPhoto: " + myPhotoRecord.toString());
		
		// Update record for self
		member.myPhotoHash = hash;
		member.generation++;
		
		// Write everything at the end
		commit(db, previousMyPhotoRecord);
		commit(db, myPhotoRecord);
	}

	private static void commit(SNetDB461 db, CommunityRecord record) throws DB461Exception {
		if (record != null) {
			db.COMMUNITYTABLE.write(record);
		}
	}
	
	private static void commit(SNetDB461 db, PhotoRecord record) throws DB461Exception {
		if (record != null) {
			if (record.hash != 0) {
				// Don't commit hashes of 0 (since 0 represents NO record)
				
				Log.d(TAG, "Committing photo record: " + record.toString());
				if (record.refCount <= 0) {
					if (record.file != null) {
						// Need to delete photo
						Log.d(TAG, "Deleting photo: " + record.file.getAbsolutePath());
						record.file.delete();
					}
					// Need to delete from DB
					db.PHOTOTABLE.delete(record.hash);
				} else {
					db.PHOTOTABLE.write(record);
				}
			}
		}
	}

	private static void decrementRefCount(PhotoRecord record) {
		if (record != null) {
			Log.d(TAG, "Decrementing refCount for " + record.hash);
			record.refCount--;
		}
	}

	private static File generatePhotoFile(int hash) {
		String resultName = "PHOTO_";
		if (hash < 0) {
			resultName += "NEG_";
		}
		resultName += Math.abs(hash);
		resultName += "_";
		Random rand = new Random();
		resultName += rand.nextInt(Integer.MAX_VALUE);
		resultName += ".jpg";
		
		File result = new File(OS.galleryDir(), resultName);
		if (result.exists()) {
			// Photo File already exists
			throw new RuntimeException("Could not save photo file: File already exists");
		}
		return result;
	}

	public static JSONObject getMembersJSON(JSONObject baseCommunity) throws DB461Exception, JSONException {
		SNetDB461 db = null;
		JSONObject result = new JSONObject();
		try {
			db = new SNetDB461();

			for (CommunityRecord member : db.COMMUNITYTABLE.readAll()) {
				// Only for debugging purposes when comparing members
				if (baseCommunity != null) {
					Log.d(TAG, "Checking member: " + member.toString());
				}
				
				if (!(baseCommunity != null && baseCommunity.has(member.name) &&
						baseCommunity.getJSONObject(member.name).getInt("generation") >= member.generation)) {
					// Only for debugging purposes when comparing members
					if (baseCommunity != null) {
						Log.d(TAG, "Adding " + member.name + " to list");
					}
					// Add member to result (only if the updates given to us do not have the same member
					// name with a generation greater than ours)
					JSONObject jsonMember = new JSONObject();
					jsonMember.put("generation", member.generation);
					jsonMember.put("myphotohash", member.myPhotoHash);
					jsonMember.put("chosenphotohash", member.chosenPhotoHash);
					result.put(member.name, jsonMember);
				}
			}
			
			return result;
		} finally {
			if (db != null) db.discard();
		}
	}
	
	private static JSONArray getNeededPhotosArray() throws DB461Exception {
		Set<Integer> neededPhotos = getNeededPhotos();
		JSONArray result = new JSONArray();
		for (int hash : neededPhotos) {
			result.put(hash);
		}
		return result;
	}
	
	private static Set<Integer> getNeededPhotos() throws DB461Exception {
		SNetDB461 db = null;
		Set<Integer> result = new HashSet<Integer>();
		try {
			db = new SNetDB461();
			
			for (CommunityRecord member : db.COMMUNITYTABLE.readFriends(self.getName())) {
				Log.d(TAG, "Found friend: " + member.name);
				
				// Check myPhoto
				PhotoRecord myPhoto = db.PHOTOTABLE.readOne(member.myPhotoHash);
				checkNeededPhoto(db, myPhoto, member.myPhotoHash, result);
				
				// Check chosenPhoto
				PhotoRecord chosenPhoto = db.PHOTOTABLE.readOne(member.chosenPhotoHash);
				checkNeededPhoto(db, chosenPhoto, member.chosenPhotoHash, result);
			}

			return result;
		} finally {
			if (db != null) db.discard();
		}
	}

	private static void checkNeededPhoto(SNetDB461 db, PhotoRecord photo, int hash, Set<Integer> result) 
										throws DB461Exception {
		if (photo == null) {
			// Shouldn't happen.  Fix problem
			photo = db.createPhotoRecord();
			photo.hash = hash;
			photo.refCount = 1;
			photo.file = null;
			commit(db, photo);
		}
		if (photo.file == null) {
			// File doesn't exist.
			if (photo.hash != 0) {
				// Only check for photos with hash not == 0
				result.add(photo.hash);
			}
		}
	}
	
	private static void incrementRefCount(PhotoRecord record) {
		if (record != null) {
			record.refCount++;
		}
	}
}
